Skip to content

feat: add StreamResponse and SSE response factories#10395

Open
michalsn wants to merge 11 commits into
codeigniter4:4.8from
michalsn:feat/stream-response
Open

feat: add StreamResponse and SSE response factories#10395
michalsn wants to merge 11 commits into
codeigniter4:4.8from
michalsn:feat/stream-response

Conversation

@michalsn

@michalsn michalsn commented Jul 8, 2026

Copy link
Copy Markdown
Member

Description
This PR adds first-class support for streaming HTTP responses. It introduces StreamResponse for responses generated progressively, such as large exports, proxied streams, or token-style output, and builds SSEResponse on top of it for Server-Sent Events. Streaming responses can be created from the response service with stream() and eventStream().

Checklist:

  • Securely signed commits
  • Component(s) with PHPDoc blocks, only if necessary or adds value (without duplication)
  • Unit testing, with >80% coverage
  • User guide updated
  • Conforms to style guide

@michalsn michalsn added new feature PRs for new features 4.8 PRs that target the `4.8` branch. labels Jul 8, 2026

@memleakd memleakd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this, looks useful! I left a few comments:

Comment thread system/HTTP/StreamResponse.php Outdated
Comment thread system/HTTP/StreamResponse.php Outdated
Comment thread user_guide_src/source/outgoing/response/041.php Outdated
Comment thread user_guide_src/source/outgoing/response/039.php Outdated

@memleakd memleakd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the updates. I left two small follow-up comments.

Comment thread system/HTTP/ResponseTrait.php Outdated
Comment thread user_guide_src/source/outgoing/response/040.php Outdated

@memleakd memleakd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the quick updates. Looks good to me!

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds first-class streaming HTTP response support to CodeIgniter’s response layer by introducing a new StreamResponse type and building SSEResponse on top of it, plus corresponding response-service factories and documentation.

Changes:

  • Added StreamResponse (streaming body via callback or iterable chunks) and refactored SSEResponse to extend it while enforcing SSE headers and skipping CSP finalization.
  • Extended ResponseInterface/ResponseTrait with stream() and eventStream() factories (including protocol version preservation) and added system tests for send/headers behavior.
  • Updated the user guide and v4.8.0 changelog to document streaming responses and the new factory methods, including examples.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
system/HTTP/StreamResponse.php Introduces StreamResponse with streaming callback/chunk support and streaming-aware send() implementation.
system/HTTP/SSEResponse.php Refactors SSE to extend StreamResponse, enforce SSE headers, and disable CSP finalization.
system/HTTP/ResponseTrait.php Adds stream() and eventStream() factories; changes shouldFinalizeCsp() visibility to support subclass control.
system/HTTP/ResponseInterface.php Requires new stream() and eventStream() methods for framework-wide response implementations.
tests/system/HTTP/StreamResponseTest.php Adds unit tests for StreamResponse behaviors and response factory return types.
tests/system/HTTP/StreamResponseSendTest.php Adds separate-process tests validating headers/cookies/body streaming behavior and CSP handling.
tests/system/HTTP/SSEResponseSendTest.php Adds a regression test for HTTP/2 protocol header behavior via eventStream() factory.
user_guide_src/source/outgoing/response.rst Adds “Streaming Responses” documentation and updates SSE section to reference stream-based design and factories.
user_guide_src/source/outgoing/response/036.php Updates SSE example to use $this->response->eventStream().
user_guide_src/source/outgoing/response/037.php Updates SSE example to use $this->response->eventStream() and then set headers.
user_guide_src/source/outgoing/response/038.php Updates SSE example to use $this->response->eventStream().
user_guide_src/source/outgoing/response/039.php Adds streaming callback example with batched writes and flush.
user_guide_src/source/outgoing/response/040.php Adds iterable/generator chunks streaming example.
user_guide_src/source/outgoing/response/041.php Adds upstream proxy streaming example (chunked forwarding).
user_guide_src/source/changelogs/v4.8.0.rst Updates changelog entries to reflect general streaming support + new factories and interface requirements.
structarmed.php Updates struct-armed configuration to account for the new StreamResponse class.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +81 to +85
public function testConstructorTreatsCallableIterableAsCallable(): void
{
// An array callable is both callable and iterable; callable must win.
$response = new StreamResponse($this->writeHello(...));
$response->pretend();
Comment on lines +821 to +834
public function stream(callable|iterable $callbackOrChunks): StreamResponse
{
return (new StreamResponse($callbackOrChunks))->setProtocolVersion($this->getProtocolVersion());
}

/**
* Creates a response for streaming Server-Sent Events (SSE).
*
* @param callable(SSEResponse): void $callback
*/
public function eventStream(callable $callback): SSEResponse
{
return (new SSEResponse($callback))->setProtocolVersion($this->getProtocolVersion());
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two silently discards state the previous response might have stored. For example, $this->response()->setHeader(...)->stream() will return a StreamResponse without the header sent. Same for ->setCookie(). If this is desired, we need to document somewhere that the previously set headers/cookies etc needs to be re-added on the returned stream or SSE response.

@paulbalandan

Copy link
Copy Markdown
Member

I asked claude to review this and I got this:

[Medium] Streaming responses get recorded as previous_url() — the NonBufferedResponseInterface marker is inert
system/HTTP/StreamResponse.php implements NonBufferedResponseInterface, but nothing in the framework consumes that marker. The skip-logic that keeps special responses out of side-effecting post-processing checks concrete classes only:

system/CodeIgniter.php:555-556! $this->response instanceof DownloadResponse && ! $this->response instanceof RedirectResponse gates storePreviousURL().
system/Debug/Toolbar.php:384 — only DownloadResponse is skipped.
StreamResponse/SSEResponse default to Content-Type: text/html (ResponseTrait.php:169) and prepareStreamHeaders() only overrides it inside send(), which runs after storePreviousURL(). storePreviousURL() stores any non-Download/Redirect, non-AJAX response whose Content-Type contains text/html.

Failure scenario: An SSEResponse is served at /events (EventSource sends no X-Requested-With, so isAJAX() is false; Content-Type is still the default text/html at that point). /events is written to _ci_previous_url. A later redirect()->back() sends the user to the SSE stream endpoint. In CI_DEBUG, the toolbar also runs full HTML injection + writes a debugbar/*.json per request for every stream. This is the exact case the unused NonBufferedResponseInterface marker was meant to cover — the deeper fix is to gate these on the marker rather than adding another concrete-class special case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

4.8 PRs that target the `4.8` branch. new feature PRs for new features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants